﻿using System;
using System.Collections.Generic;
using System.Windows;
using System.Windows.Controls;
using System.Windows.Controls.Primitives;
using System.Windows.Input;
using System.Windows.Media;

namespace SUMO.MediaBrowser.Controls.Adorners
{
    public partial class Adorner : UserControl
    {
        public delegate void UpdatedHandler();

        /// <summary>
        /// Called whenever the adorner is updated (the AdornedObject parameters change)
        /// </summary>
        public event UpdatedHandler Updated;

        private bool _canRotate = true;
        public bool CanRotate
        {
            get { return _canRotate; }
            set
            {
                if (_canRotate == value) return;
                _canRotate = value;
                rotate00.IsHitTestVisible = _canRotate;
                rotate01.IsHitTestVisible = _canRotate;
                rotate10.IsHitTestVisible = _canRotate;
                rotate11.IsHitTestVisible = _canRotate;
            }
        }

        private bool _canResize = true;
        public bool CanResize {
            get { return _canResize; }
            set {
                if (_canResize == value) return;
                _canResize = value;
                
                resize00.IsHitTestVisible = _canResize;
                resize01.IsHitTestVisible = _canResize;
                resize10.IsHitTestVisible = _canResize;
                resize11.IsHitTestVisible = _canResize;

                resizeTop.IsHitTestVisible = _canResize;
                resizeBottom.IsHitTestVisible = _canResize;
                resizeLeft.IsHitTestVisible = _canResize;
                resizeRight.IsHitTestVisible = _canResize;
            }
        }

        private bool _canMove = true;
        public bool CanMove {
            get { return _canMove; }
            set {
                if (_canMove == value) return;
                _canMove = value;

                thumbMove.IsHitTestVisible = _canMove;
            }
        }

        /// <summary>
        /// > 0 if over a thumb, 0 if not over a thumb
        /// </summary>
        private int _overThumbCurrently = 0;

        private double _x = 100;
        private double _y = 0;
        private double _angle = 0;
        private Dictionary<Thumb, DragData> _dragData = new Dictionary<Thumb, DragData>();
        private const double PI = 3.14159265358979323846264338327950288;

        private IAdornedObject _adornedObject = null;
        private IAdornerFrame _adornerFrame = null;
        private ICursor _cursor = null;

        public double X
        {
            get { return _x; }
            set
            {
                _x = value;
                trTranslate.X = _x;
            }
        }

        public double Y
        {
            get { return _y; }
            set
            {
                _y = value;
                trTranslate.Y = _y;
            }
        }

        private void InvokeUpdated()
        {
            if (Updated != null) Updated();
        }

        /// <summary>
        /// Sets the object that is displayed within this adorner
        /// The adorner will move around the object set
        /// </summary>
        /// <param name="notify">the object being adorned</param>
        public void SetAdornedObject(IAdornedObject notify)
        {
            _adornedObject = notify;
            _x = _adornedObject.X;
            _y = _adornedObject.Y;
            _angle = _adornedObject.Angle;

            Width = _adornedObject.ObjectWidth;
            Height = _adornedObject.ObjectHeight;
            trTranslate.X = _x;
            trTranslate.Y = _y;
            trRotate.Angle = _angle;
        }

        private Rect _bounds = new Rect(0, 0, 0, 0);
        private bool _hasBounds = false;

        /// <summary>
        /// Sets the bounding rectangle for this adorner
        /// The bounding rectangle is respected for move and size, but not rotate
        /// </summary>
        public void SetBounds(double x, double y, double width, double height)
        {
            _bounds.X = x;
            _bounds.Y = y;
            _bounds.Width = width;
            _bounds.Height = height;
            _hasBounds = true;
        }

        /// <summary>
        /// Removes any bounding restrictions for this adorner
        /// </summary>
        public void ResetBounds()
        {
            _hasBounds = false;
        }

        private double _zoomLevel = 1.0;

        public void SetZoom(double newZoomLevel)
        {
            _zoomLevel = newZoomLevel;
            if (_adornerFrame != null)
            {
                _adornerFrame.SetZoom(_zoomLevel);
            }
        }

        /// <summary>
        /// Sets or removes the curent adorner frame
        /// </summary>
        /// <param name="frame">the new adorner frame to use or null to not use a frame</param>
        public void SetAdornerFrame(IAdornerFrame frame)
        {
            if (_adornerFrame == frame) return;
            if (_adornerFrame != null)
            {
                gridControl.Children.Remove((UIElement)_adornerFrame);
                _adornerFrame.Detached();
            }

            _adornerFrame = frame;
            if (_adornerFrame != null)
            {
                FrameworkElement element = (FrameworkElement)_adornerFrame;
                Grid.SetRowSpan(element, 3);
                Grid.SetColumnSpan(element, 3);
                gridControl.Children.Insert(0, element);
                _adornerFrame.Attached();
                _adornerFrame.SetZoom(_zoomLevel);
                _adornerFrame.SizeChanged(Width, Height);
            }
        }

        public IAdornerFrame GetAdornerFrame() {
            return _adornerFrame;
        }

        /// <summary>
        /// Sets the object that is displayed within this adorner
        /// The adorner will move around the object set
        /// </summary>
        /// <param name="notify">the object being adorned</param>
        public void SetCursor(ICursor cursor)
        {
            _cursor = cursor;
        }

        /// <summary>
        /// Contains dragging information for a specified adorner
        /// </summary>
        private class DragData
        {
            /// <summary>
            /// Angle of the cursor based on the adorner location
            /// </summary>
            public double CursorAngle;

            /// <summary>
            /// Type of drag being performed
            /// </summary>
            public DragType DragType;

            /// <summary>
            /// true if rotating from one of the left adorners, false otherwise
            /// the (RotateIsLeft, RotateIsTop) combination is used to determine angle between the edge where the adorner is at and the center
            /// </summary>
            public bool RotateIsLeft = true;

            /// <summary>
            /// true if rotating from one of the top adorners, false otherwise
            /// the (RotateIsLeft, RotateIsTop) combination is used to determine angle between the edge where the adorner is at and the center
            /// </summary>
            public bool RotateIsTop = true;

            /// <summary>
            /// Calculated on start of rotation, this is the angle between the currently dragged adorner and the center
            /// used to "compensate" the angle between the mouse and center to ensure all 3 points: adorner, mouse, center 
            /// are on the same line
            /// </summary>
            public double RotateAdornerAngleRad = 0;

            /// <summary>
            /// Used for resize adorners only.
            /// Shows how much the width will change when movin mouse one positive unit.D:\Projects\EasyPainter\EasyPainter\Dialogs\
            /// typically -1, 0, or 1
            /// </summary>
            public double WidthChange = 0;

            /// <summary>
            /// Used for resize adorners only.
            /// Shows how much the height will change when movin mouse one positive unit.
            /// typically -1, 0, or 1 
            /// </summary>
            public double HeightChange = 0;

            /// <summary>
            /// Selects left or right edge to not move during resize
            /// -1: right
            /// 1: left
            /// </summary>
            public double HSignInverse = 1;

            /// <summary>
            /// Selects top or bottom edge to not move during resize
            /// -1: top
            /// 1: bottom
            /// </summary>
            public double VSign = 0;
        }

        private enum DragType
        {
            None,
            Size,
            Rotate,
            Move
        }

        private DragType _currentDragOperation = DragType.None;

        public Adorner()
        {
            InitializeComponent();

            _dragData.Add(resize00, new DragData() { CursorAngle = 45, DragType = DragType.Size, WidthChange = -1, HeightChange = -1, HSignInverse = -1, VSign = 1 });
            _dragData.Add(resize10, new DragData() { CursorAngle = 135, DragType = DragType.Size, WidthChange = 1, HeightChange = -1, HSignInverse = 1, VSign = 1 });
            _dragData.Add(resize01, new DragData() { CursorAngle = 135, DragType = DragType.Size, WidthChange = -1, HeightChange = 1, HSignInverse = -1, VSign = -1 });
            _dragData.Add(resize11, new DragData() { CursorAngle = 45, DragType = DragType.Size, WidthChange = 1, HeightChange = 1, HSignInverse = 1, VSign = -1 });

            _dragData.Add(resizeTop, new DragData() { CursorAngle = 90, DragType = DragType.Size, WidthChange = 0, HeightChange = -1, HSignInverse = -1, VSign = 1 });
            _dragData.Add(resizeBottom, new DragData() { CursorAngle = 90, DragType = DragType.Size, WidthChange = 0, HeightChange = 1, HSignInverse = 1, VSign = -1 });
            _dragData.Add(resizeLeft, new DragData() { CursorAngle = 0, DragType = DragType.Size, WidthChange = -1, HeightChange = 0, HSignInverse = -1, VSign = 1 });
            _dragData.Add(resizeRight, new DragData() { CursorAngle = 0, DragType = DragType.Size, WidthChange = 1, HeightChange = 0, HSignInverse = 1, VSign = -1 });

            _dragData.Add(rotate00, new DragData() { CursorAngle = 0, DragType = DragType.Rotate, RotateIsLeft = true, RotateIsTop = true });
            _dragData.Add(rotate10, new DragData() { CursorAngle = 90, DragType = DragType.Rotate, RotateIsLeft = false, RotateIsTop = true });
            _dragData.Add(rotate01, new DragData() { CursorAngle = 270, DragType = DragType.Rotate, RotateIsLeft = true, RotateIsTop = false });
            _dragData.Add(rotate11, new DragData() { CursorAngle = 180, DragType = DragType.Rotate, RotateIsLeft = false, RotateIsTop = false });

            _dragData.Add(thumbMove, new DragData() { CursorAngle = 0, DragType = DragType.Move });
        }

        #region Rotate
        Point _mousePosRelativeToThis;
        private void rotate_DragStarted(object sender, System.Windows.Controls.Primitives.DragStartedEventArgs e)
        {
            _currentDragOperation = DragType.Rotate;
            _draggedThumb = (Thumb)sender;
            Point result = trRotate.Inverse.Transform(new Point(e.HorizontalOffset - _x, e.VerticalOffset - _y));
            _mousePosRelativeToThis.X = result.X;
            _mousePosRelativeToThis.Y = result.Y;
            DragData data = _dragData[_draggedThumb];
            data.RotateAdornerAngleRad = GetRotateAdornerAngle(data, Width / 2, Height / 2);
        }
        #endregion


        #region Dragging and Cursors
        private void thumb_MouseEnter(object sender, MouseEventArgs e)
        {
            _overThumbCurrently++;
            Thumb thumb = (Thumb)sender;
            DragData data = _dragData[thumb];
            ShowCursor(data.DragType, e.GetPosition(this), data.CursorAngle);
        }

        private void thumb_MouseLeave(object sender, MouseEventArgs e)
        {
            _overThumbCurrently--;
            if (_draggedThumb == null)
            {
                ShowDefaultCursor();
            }
        }

        Point _lastMousePos;
        bool _lastMousePosInitialized = false;

        /// <summary>
        /// Retrieves the angle between the edge for a given rotate adorner and the center
        /// </summary>
        /// <param name="data"></param>
        /// <param name="centerX"></param>
        /// <param name="centerY"></param>
        /// <returns></returns>
        private double GetRotateAdornerAngle(DragData data, double centerX, double centerY)
        {
            double x, y;
            if (data.RotateIsLeft)
            {
                x = centerX;
            }
            else
            {
                x = -centerX;
            }

            if (data.RotateIsTop)
            {
                y = centerY;
            }
            else
            {
                y = -centerY;
            }

            return Math.Atan2(x, y) - PI;
        }

        private double RadFromDeg(double angle)
        {
            return angle * PI / 180;
        }

        private double DegFromRad(double angle)
        {
            return angle * 180 / PI;
        }

        private void thumb_MouseMove(object sender, MouseEventArgs e)
        {
            DragData dragData;
            _mousePosRelativeToThis = e.GetPosition(this);
            if (!_lastMousePosInitialized)
            {
                _lastMousePos.X = _mousePosRelativeToThis.X;
                _lastMousePos.Y = _mousePosRelativeToThis.Y;
                _lastMousePosInitialized = true;
                return;
            }
            Point delta = new Point();
            delta.X = _mousePosRelativeToThis.X - _lastMousePos.X;
            delta.Y = _mousePosRelativeToThis.Y - _lastMousePos.Y;
            UpdateCursorPos(_mousePosRelativeToThis);
            switch (_currentDragOperation)
            {
                case DragType.Move:
                    if (_hasBounds)
                    {
                        double newX = _x + delta.X;
                        double newY = _y + delta.Y;
                        if (newX < _bounds.X)
                        {
                            _x = _bounds.X;
                        }
                        else
                        {
                            if ((newX + Width) > _bounds.Right)
                            {
                                _x = _bounds.Right - Width;
                            }
                            else
                            {
                                _x = newX;
                            }
                        }

                        if (newY < _bounds.Y)
                        {
                            _y = _bounds.Y;
                        }
                        else
                        {
                            if ((newY + Height) > _bounds.Bottom)
                            {
                                _y = _bounds.Bottom - Height;
                            }
                            else
                            {
                                _y = newY;
                            }
                        }
                    }
                    else
                    {
                        _x += delta.X;
                        _y += delta.Y;
                    }
                    trTranslate.X = _x;
                    trTranslate.Y = _y;
                    if (_adornedObject != null)
                    {
                        _adornedObject.X = _x;
                        _adornedObject.Y = _y;
                    }
                    InvokeUpdated();
                    break;

                case DragType.Rotate:
                    {
                        double w2 = Width / 2;
                        double h2 = Height / 2;
                        Point centerOfRotation = new Point(_x + w2, _y + h2);
                        dragData = _dragData[_draggedThumb];
                        // calculate the angle between the mouse and the center of rotation and subtract the adorner angle to make sure
                        // the adorner, mouse and center of rotation will end up on the same line
                        _angle = DegFromRad(Math.Atan2(centerOfRotation.X - _mousePosRelativeToThis.X, -centerOfRotation.Y + _mousePosRelativeToThis.Y) +
                            dragData.RotateAdornerAngleRad);
                        trRotate.Angle = _angle;
                        if (_adornedObject != null)
                        {
                            _adornedObject.Angle = _angle;
                        }
                        UpdateCursorAngle();
                        InvokeUpdated();
                    }
                    break;

                case DragType.Size:
                    // resize algorithm: 
                    // 1. find the distance between the line passing through the point the mouse was last at 
                    //   and the current point - this is the distance to move the edge
                    // 2. Move the edge based on the current adorner (dragData)
                    Point rotatedDelta = trRotate.Inverse.Transform(delta);
                    dragData = _dragData[_draggedThumb];

                    double widthInc = rotatedDelta.X;
                    double heightInc = rotatedDelta.Y;

                    if (_preserveCenterOnResize)
                    {
                        // since we preserve the center, 
                        // width and height change should be double when the mouse if moved one pixel 
                        // so that the cursor will follow the mouse
                        widthInc *= dragData.WidthChange * 2;
                        heightInc *= dragData.HeightChange * 2;

                        // check for min width and height
                        if (Width + widthInc < 0)
                        {
                            widthInc = -Width;
                        }
                        if (Height + heightInc < 0)
                        {
                            heightInc = -Height;
                        }

                        double widthInc2 = widthInc / 2;
                        double heightInc2 = heightInc / 2;

                        Width += widthInc;
                        Height += heightInc;

                        // the difference between the center positions before the rotation is (-widthInc2, -widthInc2)
                        // the center is moved by (-widthInc2, -widthInc2) in order to compensate for the difference,
                        // which effectively will preserve the center
                        _x -= widthInc2;
                        _y -= heightInc2;
                    }
                    else
                    {
                        widthInc *= dragData.WidthChange;
                        heightInc *= dragData.HeightChange;

                        // check for min width and height
                        if (Width + widthInc < 0)
                        {
                            widthInc = -Width;
                        }
                        if (Height + heightInc < 0)
                        {
                            heightInc = -Height;
                        }

                        double widthInc2 = widthInc / 2;
                        double heightInc2 = heightInc / 2;

                        // HSign, VSign combinations:
                        // +, - = top-left
                        // -, - = top-right
                        // +, + = bototm-left
                        // -, + = bottom-right

                        Point vectorToNewCenterAfterRotation = new Point(dragData.HSignInverse * widthInc2, dragData.VSign * heightInc2);
                        Point pointToKeepOrigAfterSize = trRotate.Inverse.Transform(vectorToNewCenterAfterRotation);

                        double dx = pointToKeepOrigAfterSize.X - widthInc2;
                        double dy = pointToKeepOrigAfterSize.Y + heightInc2;

                        Width += widthInc;
                        Height += heightInc;
                        _x += dx;
                        _y -= dy;
                    }

                    // check for bounds: note if we have bounds, we assume rotation angle is zero
                    if (_hasBounds)
                    {
                        if (_x < _bounds.X)
                        {
                            Width -= (_bounds.X - _x);
                            _x = _bounds.X;
                        }

                        if ((_x + Width) > _bounds.Right)
                        {
                            Width = _bounds.Right - _x;
                        }

                        if (_y < _bounds.Y)
                        {
                            Height -= (_bounds.Y - _y);
                            _y = _bounds.Y;
                        }

                        if ((_y + Height) > _bounds.Bottom)
                        {
                            Height = _bounds.Bottom - _y;
                        }
                    }

                    trTranslate.X = _x;
                    trTranslate.Y = _y;
                    if (_adornedObject != null)
                    {
                        _adornedObject.X = _x;
                        _adornedObject.Y = _y;
                        _adornedObject.ObjectWidth = Width;
                        _adornedObject.ObjectHeight = Height;
                    }
                    InvokeUpdated();
                    break;

            }
            _lastMousePos.X = _mousePosRelativeToThis.X;
            _lastMousePos.Y = _mousePosRelativeToThis.Y;
        }

        bool _preserveCenterOnResize = false;

        public void StartMoveDrag()
        {
            _lastMousePosInitialized = false;
            thumb_DragStarted(thumbMove, null);
        }

        private void thumb_DragStarted(object sender, System.Windows.Controls.Primitives.DragStartedEventArgs e)
        {
            _currentDragOperation = _dragData[(Thumb)sender].DragType;
            _draggedThumb = (Thumb)sender;
        }

        private void thumb_DragCompleted(object sender, System.Windows.Controls.Primitives.DragCompletedEventArgs e)
        {
            _currentDragOperation = DragType.None;
            _draggedThumb = null;
            if (_overThumbCurrently == 0)
            { // if not over thumb
                ShowDefaultCursor();
            }
        }

        private void thumbMove_MouseLeftButtonUp(object sender, MouseButtonEventArgs e)
        {
            _currentDragOperation = DragType.None;
            _draggedThumb = null;
            if (_overThumbCurrently == 0)
            { // if not over thumb
                ShowDefaultCursor();
            }
        }

        private void ShowDefaultCursor()
        {
            _cursor.Show(CursorType.Default);
        }

        private void ShowCursor(DragType type, Point pos, double angle)
        {
            CursorType cursorType;
            switch (type)
            {
                case DragType.Move:
                    cursorType = CursorType.Move;
                    break;

                case DragType.Rotate:
                    cursorType = CursorType.Rotate;
                    break;

                case DragType.Size:
                    cursorType = CursorType.Size;
                    break;

                case DragType.None:
                    cursorType = CursorType.Default;
                    break;

                default:
                    throw new ArgumentException("Unknown drag type: " + type.ToString());
            }

            _cursor.Show(cursorType);
            _currentCursorData.InitAngle = angle;

            UpdateCursorAngle();
            UpdateCursorPos(pos);
        }

        private void UpdateCursorAngle()
        {
            _cursor.SetAngle(_currentCursorData.InitAngle + _angle);
        }
        
        private Point _posOffset = new Point(0, 0);

        public void SetCursorPosOffset(Point offset)
        {
            _posOffset = offset;
        }

        private void UpdateCursorPos(Point pos)
        {
            _cursor.SetPos(pos.X + _posOffset.X, pos.Y + _posOffset.Y);
        }

        Thumb _draggedThumb = null;
        CursorData _currentCursorData = new CursorData();
        #endregion

        private void gridControl_SizeChanged(object sender, SizeChangedEventArgs e)
        {
            if (_adornerFrame != null)
            {
                // note: do not use e.NewSize.Width, e.NewSize.Height, since they are truncated and precision may be lost
                _adornerFrame.SizeChanged(Width, Height);
            }
        }

        private int RobustRound(double value)
        {
            int valueInt = (int)value;
            double frac = value - valueInt;
            if (frac >= 0.5) return valueInt + 1;
            return valueInt;
        }

        private class CursorData
        {
            public double InitAngle;
        }
    }
}
